Chapter 7

Networking and REST APIs

Session 7

Learning Objectives

By the end of this chapter, you will be able to:

1

Introduction to Networking in Flutter

Most mobile apps need to communicate with backend servers to fetch data, submit forms, and sync state. Flutter provides excellent support for HTTP networking through packages like http and dio. This chapter focuses on the standard http package for making REST API calls.

Key concept: All network operations in Flutter are asynchronous. Use async/await or Future chains to handle network responses without blocking the UI thread.

2

Setting Up the HTTP Package

Add Dependency

Add the http package to your pubspec.yaml:

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0

Import in Dart File

import 'package:http/http.dart' as http;
import 'dart:convert';
3

Making HTTP Requests

GET Request

Fetch data from a server:

Basic GET Request

Future fetchData() async {
  final url = Uri.parse('https://api.example.com/users');
  final response = await http.get(url);
  
  if (response.statusCode == 200) {
    // Success
    final data = jsonDecode(response.body);
    print(data);
  } else {
    // Error
    print('Error: ${response.statusCode}');
  }
}

POST Request

Send data to a server:

POST Request with JSON Body

Future createUser(String name, String email) async {
  final url = Uri.parse('https://api.example.com/users');
  final response = await http.post(
    url,
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({
      'name': name,
      'email': email,
    }),
  );
  
  if (response.statusCode == 201) {
    final newUser = jsonDecode(response.body);
    print('Created: $newUser');
  }
}

PUT and DELETE Requests

Update and delete resources:

PUT and DELETE Examples

// Update user
Future updateUser(String userId, Map data) async {
  final url = Uri.parse('https://api.example.com/users/$userId');
  final response = await http.put(
    url,
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode(data),
  );
}

// Delete user
Future deleteUser(String userId) async {
  final url = Uri.parse('https://api.example.com/users/$userId');
  final response = await http.delete(url);
  
  if (response.statusCode == 204) {
    print('User deleted');
  }
}
4

Adding Headers and Authentication

Request Headers

Add custom headers for authentication, content type, and API keys:

Headers Example

Future fetchWithAuth(String token) async {
  final url = Uri.parse('https://api.example.com/protected');
  final response = await http.get(
    url,
    headers: {
      'Authorization': 'Bearer $token',
      'Content-Type': 'application/json',
      'X-API-Key': 'your-api-key',
    },
  );
}

Query Parameters

Add query parameters to URLs:

Query Parameters

Future searchUsers(String query, int page) async {
  final url = Uri.parse('https://api.example.com/users').replace(
    queryParameters: {
      'q': query,
      'page': page.toString(),
      'limit': '20',
    },
  );
  final response = await http.get(url);
}
5

Parsing JSON Responses

Basic JSON Parsing

Convert JSON strings to Dart objects:

Simple JSON Parsing

// JSON string
final jsonString = '{"name": "John", "age": 30}';

// Parse to Map
final Map json = jsonDecode(jsonString);
print(json['name']); // John
print(json['age']);  // 30

// Convert back to JSON
final String jsonOutput = jsonEncode(json);

Creating Model Classes

Create Dart classes to represent API data:

User Model Example

class User {
  final String id;
  final String name;
  final String email;
  
  User({
    required this.id,
    required this.name,
    required this.email,
  });
  
  // Factory constructor from JSON
  factory User.fromJson(Map json) {
    return User(
      id: json['id'] as String,
      name: json['name'] as String,
      email: json['email'] as String,
    );
  }
  
  // Convert to JSON
  Map toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
    };
  }
}

// Usage
Future> fetchUsers() async {
  final response = await http.get(Uri.parse('https://api.example.com/users'));
  if (response.statusCode == 200) {
    final List jsonList = jsonDecode(response.body);
    return jsonList.map((json) => User.fromJson(json)).toList();
  } else {
    throw Exception('Failed to load users');
  }
}
6

Error Handling

Network Error Handling

Handle various types of network errors:

Comprehensive Error Handling

Future> fetchUsersSafe() async {
  try {
    final response = await http.get(
      Uri.parse('https://api.example.com/users'),
    ).timeout(Duration(seconds: 10));
    
    if (response.statusCode == 200) {
      final List jsonList = jsonDecode(response.body);
      return jsonList.map((json) => User.fromJson(json)).toList();
    } else if (response.statusCode == 401) {
      throw Exception('Unauthorized - please login');
    } else if (response.statusCode == 404) {
      throw Exception('Resource not found');
    } else {
      throw Exception('Server error: ${response.statusCode}');
    }
  } on SocketException {
    throw Exception('No internet connection');
  } on TimeoutException {
    throw Exception('Request timeout - please try again');
  } on FormatException {
    throw Exception('Invalid response format');
  } catch (e) {
    throw Exception('Unexpected error: $e');
  }
}

Required Imports

import 'dart:io'; // for SocketException
import 'dart:async'; // for TimeoutException
7

Building a Service Layer

API Service Pattern

Create a dedicated service class to centralize API calls:

ApiService Example

class ApiService {
  static const String baseUrl = 'https://api.example.com';
  String? _authToken;
  
  void setAuthToken(String token) {
    _authToken = token;
  }
  
  Map get _headers => {
    'Content-Type': 'application/json',
    if (_authToken != null) 'Authorization': 'Bearer $_authToken',
  };
  
  Future> getUsers() async {
    final response = await http.get(
      Uri.parse('$baseUrl/users'),
      headers: _headers,
    );
    
    if (response.statusCode == 200) {
      final List jsonList = jsonDecode(response.body);
      return jsonList.map((json) => User.fromJson(json)).toList();
    }
    throw _handleError(response);
  }
  
  Future createUser(User user) async {
    final response = await http.post(
      Uri.parse('$baseUrl/users'),
      headers: _headers,
      body: jsonEncode(user.toJson()),
    );
    
    if (response.statusCode == 201) {
      return User.fromJson(jsonDecode(response.body));
    }
    throw _handleError(response);
  }
  
  Exception _handleError(http.Response response) {
    switch (response.statusCode) {
      case 400:
        return Exception('Bad request');
      case 401:
        return Exception('Unauthorized');
      case 404:
        return Exception('Not found');
      case 500:
        return Exception('Server error');
      default:
        return Exception('Error: ${response.statusCode}');
    }
  }
}
8

Loading States and UI Integration

Managing Loading States

Use StatefulWidget to manage loading, error, and data states:

Loading State Pattern

class UsersScreen extends StatefulWidget {
  @override
  _UsersScreenState createState() => _UsersScreenState();
}

class _UsersScreenState extends State {
  List _users = [];
  bool _isLoading = false;
  String? _error;
  final _apiService = ApiService();
  
  @override
  void initState() {
    super.initState();
    _loadUsers();
  }
  
  Future _loadUsers() async {
    setState(() {
      _isLoading = true;
      _error = null;
    });
    
    try {
      final users = await _apiService.getUsers();
      setState(() {
        _users = users;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    if (_isLoading) {
      return Center(child: CircularProgressIndicator());
    }
    
    if (_error != null) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Error: $_error'),
            ElevatedButton(
              onPressed: _loadUsers,
              child: Text('Retry'),
            ),
          ],
        ),
      );
    }
    
    return ListView.builder(
      itemCount: _users.length,
      itemBuilder: (context, index) {
        final user = _users[index];
        return ListTile(
          title: Text(user.name),
          subtitle: Text(user.email),
        );
      },
    );
  }
}
9

Best Practices

Networking Best Practices

  • Always handle errors and provide user-friendly error messages.
  • Add timeouts to prevent indefinite waiting.
  • Use try-catch blocks around network calls.
  • Centralize API calls in a service layer for reusability.
  • Validate JSON structure before parsing to avoid crashes.
  • Store base URLs and endpoints as constants.
  • Use proper HTTP status codes to determine success/failure.
  • Implement retry logic for transient failures.
  • Cache responses when appropriate to reduce network calls.
  • Never make network calls in build() method.
10

Exercises

1. Basic API Client

Create an ApiService class that fetches a list of posts from JSONPlaceholder API (https://jsonplaceholder.typicode.com/posts). Create a Post model class with id, title, and body fields. Display the posts in a ListView with proper loading and error states.

2. CRUD Operations

Extend the ApiService to support creating, updating, and deleting posts. Add UI buttons to test each operation and show success/error messages.

3. Error Handling and Retry

Implement a retry mechanism that attempts the request up to 3 times with exponential backoff. Add a retry button in the error UI that manually retries failed requests.